<?php
/**
 * [LupMisNotAllowedSync]
 * 本代码为系统自动生成代码，请根据自己业务进行修改;
 * 生成时间 2022-02-01 22:12:12;
 * 版权所有 2020-2022 lizhongwen，并保留所有权利;
 * 说明: 这不是一个自由软件！您只能在不用于商业目的的前提下对程序代码进行修改和使用；不允许对程序代码以任何形式任何目的的再发布;
 * 作者: 中文Lee;
 * 作者主页: http://www.lizhongwen.com;
 * 邮箱: 360811363@qq.com;
 * QQ: 360811363;
 */

namespace application\core;

use Exception;

class SnowFlake
{
    // 开始时间,固定一个小于当前时间的毫秒数即可
    const epoch = 1640966400000;

    // 机器标识占的位数
    const workerIdBits = 5;

    // 数据中心标识占的位数
    const datacenterIdBits = 5;

    // 毫秒内自增数点的位数
    const sequenceBits = 12;

    //工作机器id 0~32
    protected $workId = 0;

    //数据中心id 0~32
    protected $datacenterId = 0;

    static $lastTimestamp = -1;

    //同毫秒内自增序列
    static $sequence = 0;

    private static $obj;

    private $redis;

    /**
     * @throws Exception
     */
    function __construct($workId, $datacenterId)
    {
        if (REDIS_ENABLE) {
            $this->redis = RedisUtil::instance();
            $snowFlakeLastTimestamp = $this->redis->get("snowflake_last_timestamp");
            if (!empty($snowFlakeLastTimestamp)) {
                self::$lastTimestamp = $snowFlakeLastTimestamp;
            }
        }
        // 机器ID范围判断
        $maxWorkerId = -1 ^ (-1 << self::workerIdBits);
        if ($workId > $maxWorkerId || $workId < 0) {
            throw new Exception("workerId can't be greater than maxWorkerId or less than 0");
        }
        // 数据中心ID范围判断
        $maxDatacenterId = -1 ^ (-1 << self::datacenterIdBits);
        if ($datacenterId > $maxDatacenterId || $datacenterId < 0) {
            throw new Exception("datacenter Id can't be greater than " . $maxDatacenterId . " or less than 0");
        }
        // 赋值
        $this->workId = $workId;
        $this->datacenterId = $datacenterId;
    }

    public static function instance(): SnowFlake
    {
        if (!isset(self::$obj) || is_null(self::$obj)) {
            self::$obj = new self(SNOWFLAKE_WORK_ID, SNOWFLAKE_DATA_CENTER_ID);
        }
        return self::$obj;
    }

    // 生成一个ID

    public function nextId()
    {
        $timestamp = $this->timeGen();
        $lastTimestamp = self::$lastTimestamp;
        // 判断时钟是否正常
        if ($timestamp < $lastTimestamp) {
            return Result::fail(false, [], "Clock moved backwards.  Refusing to generate id for %d milliseconds", ($lastTimestamp - $timestamp));
        }
        // 生成唯一序列
        if ($lastTimestamp == $timestamp) {
            $sequenceMask = -1 ^ (-1 << self::sequenceBits);
            self::$sequence = (self::$sequence + 1) & $sequenceMask;
            if (self::$sequence == 0) {
                $timestamp = $this->tilNextMillis($lastTimestamp);
            }
        } else {
            self::$sequence = 0;
        }
        self::$lastTimestamp = $timestamp;
        // 时间毫秒/数据中心ID/机器ID,要左移的位数
        $timestampLeftShift = self::sequenceBits + self::workerIdBits + self::datacenterIdBits;
        $datacenterIdShift = self::sequenceBits + self::workerIdBits;
        $workerIdShift = self::sequenceBits;
        // 组合4段数据返回: 时间戳.数据标识.工作机器.序列
        $nextId = (($timestamp - self::epoch) << $timestampLeftShift) | ($this->datacenterId << $datacenterIdShift) | ($this->workId << $workerIdShift) | self::$sequence;
        // 更新redis 最后的时间戳
        if (REDIS_ENABLE) {
            $lastId = $this->redis->get("snowflake_last_id");
            if (!empty($lastId)) {
                if ($lastId >= $nextId) {
                    //如果插入并发高可能会导致id滞后，所以这里继续再进行拨号一次，直到拨到的号小于当前id
                    $nextId = $this->nextId();
                }
            }
            $this->redis->set("snowflake_last_timestamp", $timestamp);
            $this->redis->set("snowflake_last_id", $nextId);
        }
        return $nextId;
    }

    // 取当前时间毫秒
    protected function timeGen(): float
    {
        return (float)sprintf("%.0f", microtime(true) * 1000);
    }

    // 取下一毫秒
    protected function tilNextMillis($lastTimestamp): float
    {
        $timestamp = $this->timeGen();
        while ($timestamp <= $lastTimestamp) {
            $timestamp = $this->timeGen();
        }
        return $timestamp;
    }
}